/******************************************************************************
 * This program is a 100% Java Email Server.
 ******************************************************************************
 * Copyright (c) 2001-2011, Eric Daugherty (http://www.ericdaugherty.com)
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   * Neither the name of the copyright holder nor the
 *     names of its contributors may be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ''AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 ******************************************************************************
 * For current versions and more information, please visit:
 * http://javaemailserver.sf.net/
 *
 * or contact the authors at:
 * java@ericdaugherty.com
 * andreaskyrmegalos@hotmail.com
 *
 ******************************************************************************
 * This program is based on the CSRMail project written by Calvin Smith.
 * http://crsemail.sourceforge.net/
 ******************************************************************************
 *
 * $Rev$
 * $Date$
 *
 ******************************************************************************/
package com.ericdaugherty.mail.server.configuration;

//Java imports
import java.security.*;

//Logging imports
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 *
 * @author Andreas Kyrmegalos
 */
public class PasswordFactory {

   /** Logger Category for this class. */
   private static Log log = LogFactory.getLog(PasswordFactory.class);
   private static PasswordFactory instance;

   public static void instantiate(BackEndTypeEnum backEndType) {

      if (instance == null) {
         instance = new PasswordFactory(backEndType);
      }
   }

   public static void shutdown() {

      instance = null;
   }

   public static PasswordFactory getInstance() {

      return instance;
   }
   private final PasswordHasherGetter peg;

   private PasswordFactory(BackEndTypeEnum backEndType) {

      if (backEndType == BackEndTypeEnum.FILE) {
         peg = new FilePasswordHasherGetter();
      } else {
         peg = new DbPasswordHasherGetter();
      }
   }

   public final PasswordHasher getPasswordHasher() {

      return peg.getHasher();
   }

   private final class DbPasswordHasherGetter extends PasswordHasherGetter {

      public final PasswordHasher getHasher() {

         return new DbPasswordHasher();
      }
   }

   private final class FilePasswordHasherGetter extends PasswordHasherGetter {

      public final PasswordHasher getHasher() {

         return new FilePasswordHasher();
      }
   }

   private abstract class PasswordHasherGetter {

      public abstract PasswordHasher getHasher();
   }

   private final class DbPasswordHasher extends AbstractPasswordHasher {

      private byte[] salt;

      @Override
      public final void setSalt(String salt) {

         this.salt = stringToByte(salt, 16);
      }

      @Override
      public final String getSalt() {

         return byteToString(salt, 48);
      }

      public final String hashPassword(final String password) {

         try {

            if (salt == null) {

               salt = new byte[16];

               SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
               sr.setSeed(System.currentTimeMillis());
               sr.nextBytes(salt);
            }

            MessageDigest md = MessageDigest.getInstance("SHA");

            md.update(salt);
            md.update(password.getBytes("UTF-8"));

            byte[] hash = md.digest();
            for (int i = 0; i < (1999); i++) {
               md.reset();
               hash = md.digest(hash);
            }

            return byteToString(hash, 60);
         } catch (Exception exception) {

            log.error(exception);
            return null;
         }
      }

      public final boolean passwordMatches(String storedPassword, String password) {

         if (salt == null) {
            return false;
         }

         return storedPassword.equals(hashPassword(password));
      }
   }

   private final class FilePasswordHasher extends AbstractPasswordHasher {

      public final void setSalt(String salt) {
      }

      public final String getSalt() {
         return null;
      }

      public final String hashPassword(String password) {

         try {
            MessageDigest md = MessageDigest.getInstance("SHA");

            //Create the encrypted Byte[]
            md.update(password.getBytes());
            byte[] hash = md.digest();

            return "{SHA}" + byteToString(hash, 60);
         } catch (NoSuchAlgorithmException nsae) {
            log.error("Error getting password hash - " + nsae.getMessage());
            return null;
         }
      }

      public final boolean passwordMatches(String storedPassword, String password) {

         return storedPassword.equals(hashPassword(password));
      }
   }

   private abstract class AbstractPasswordHasher implements PasswordHasher {

      public abstract void setSalt(String salt);

      public abstract String getSalt();

      public abstract String hashPassword(String password);

      public final String hashRealmPassword(String username, String realm, String password) throws GeneralSecurityException {

         MessageDigest md = null;
         md = MessageDigest.getInstance("MD5");
         md.update(username.getBytes());
         md.update(":".getBytes());
         md.update(realm.getBytes());
         md.update(":".getBytes());
         md.update(password.getBytes());
         byte[] hash = md.digest();
         return toHex(hash, hash.length);
      }

      public abstract boolean passwordMatches(String storedPassword, String password);

      //Convert the String into a byte array
      protected final byte[] stringToByte(String string, int size) {

         byte[] bytes = new byte[size];

         for (int i = 0; i < string.length(); i += 3) {

            bytes[i / 3] = Byte.parseByte("" + (Integer.valueOf(string.substring(i, i + 3)) - 128));
         }

         return bytes;

      }

      //Convert the byte array into a String
      protected final String byteToString(byte[] hash, int size) {

         StringBuilder hashStringBuf = new StringBuilder(size);
         String byteString;
         int byteLength;

         for (int index = 0; index < hash.length; index++) {

            byteString = String.valueOf(hash[index] + 128);

            //Pad string to 3.  Otherwise hash may not be unique.
            byteLength = byteString.length();
            switch (byteLength) {
               case 1:
                  byteString = "00" + byteString;
                  break;
               case 2:
                  byteString = "0" + byteString;
                  break;
            }
            hashStringBuf.append(byteString);
         }

         return hashStringBuf.toString();
      }

      private String toHex(byte[] b, int len) {
         if (b == null) {
            return "";
         }
         StringBuilder s = new StringBuilder(96);
         int i;
         for (i = 0; i < len; i++) {
            s.append(toHex(b[i]));
         }
         return s.toString();
      }

      private String toHex(byte b) {
         int i = new Integer((((int) b) << 24) >>> 24).intValue();

         if (i < (byte) 16) {
            return "0" + Integer.toString(i, 16);
         } else {
            return Integer.toString(i, 16);
         }
      }
   }
}
